# -*- coding: utf-8 -*-

from StringIO import StringIO
import itertools
import logging
import optparse
import os
import shutil
import sys
from uuid import uuid4 as get_uuid


import ws9.file # Shortcut to import all ws9 submodules: ``file`` imports all.

ENDINGS = {
    'unix': '\n',
    'mac': '\r',
    'dos': '\r\n',
    'lf': '\n',
    'cr': '\r',
    'crlf': '\r\n' }

INDENTS = {
    's': ' ',
    't': '\t',
    'space': ' ',
    'tab': '\t' }


PARSER = optparse.OptionParser()
PARSER.add_option('-e', '--endings',
    dest='endings', metavar='FORMAT', default=None,
    help='Convert line endings to FORMAT (case-insensitive). '
'FORMAT may be one of "unix", "mac", "dos", "lf", "cr" or "crlf". '
'If FORMAT is invalid, no change of line endings will occur.')
PARSER.add_option('-i', '--indent-type',
    dest='indent_type', metavar='FORMAT', default=None,
    help='Convert indentation to FORMAT (case-insensitive). '
'FORMAT may be one of "s" (spaces) or "t" (tabs). '
'If FORMAT is invalid, no change of indentation will occur.')
PARSER.add_option('-w', '--tab-width',
    dest='tab_width', metavar='WIDTH', default='4',
    help='Interpret one tab character as WIDTH spaces. Defaults to 4. '
'If an invalid WIDTH is given, the default will be used.')
PARSER.add_option('-o', '--output',
    dest='fileout', metavar='FILE', default=None,
    help='Write output to FILE. Default behaviour is to overwrite input file. '
'If FILE is "-", write to standard output. Not compatible with -r or -a. '
'It is also invalid to specify -o with multiple input files. In the case of '
'invalid usage, the option will be ignored.')
PARSER.add_option('-r', '--recursive',
    dest='recursive', action='store_true', default=False,
    help='Recurse through given directories, changing all files found.'
'Default behaviour is to overwrite files found, unless -a is specified.')
PARSER.add_option('-a', '--append',
    dest='append', metavar='STRING', default=None,
    help='If given, then STRING will be appended to the filenames of all '
'files which have been changed, and the output written to the new filename. '
'Not compatible with -o: -o will take precedence over this option, if given.')
PARSER.add_option('-m', '--in-memory',
    dest='in_memory', action='store_true', default=False,
    help='When overwriting files, read the file into memory, perform the '
'changes, then write back to the file. The default is to create a temporary '
'filename, move the original to that filename, and then write the changes '
'line-by-line to the original filename. This option stops the creation of '
'temporary files, but may consume a lot of memory.')
PARSER.add_option('-I', '--ignore',
    dest='ignore', action='store', metavar='ENDINGS', default='pyc,pyo,pyd',
    help='Ignore all files whose names end in one of the given strings. These '
'strings should be given as part of a comma-delimited list, which will '
'default to "pyc,pyo,pyd", meaning compiled Python files will not be'
'converted.')
PARSER.add_option('-d', '--debug',
    dest='debug', action='store_true', default=False,
    help='Activate debug mode: report all filenames being changed to stdout.')


def validate_options((options, args)):
    if len(args) == 0:
        args = ['-']
        logging.disable(51) # Disable ALL logging.
    elif options.debug:
        logging.basicConfig(level=logging.DEBUG,
            format='%(levelname)s: %(message)s')
    else:
        logging.basicConfig(level=logging.WARNING,
            format='%(levelname)s: %(message)s')
    if options.fileout and (options.recursive or (len(args) > 1)):
        logging.warn('Invalid --output option: ignoring.')
        options.fileout = None
    if options.recursive and args == ['-']:
        options.recursive = False
    if args == ['-'] and not options.fileout:
        options.fileout = '-'
    if len(args) > 1 and '-' in args:
        args.remove('-')
    if options.endings:
        if options.endings.lower() not in ENDINGS.keys():
            logging.warn('Invalid --endings option: ignoring.')
        options.endings = ENDINGS.get(options.endings.lower(), None)
    if options.indent_type:
        if options.indent_type.lower() not in INDENTS.keys():
            logging.warn('Invalid --indent-type option: ignoring.')
        options.indent_type = INDENTS.get(options.indent_type.lower(), None)
    if options.fileout and options.append and (len(args) == 1):
        logging.warn('Invalid --append option: ignoring.')
        options.append = None
    options.delete = {}
    return options, args


def get_files(options, args):
    ignore_extensions = options.ignore.split(',')
    if options.recursive:
        value = itertools.chain(*(
            list_files(arg) for arg in
            itertools.ifilter(is_dir_or_warn, args)))
    else:
        value = itertools.ifilter(is_file_or_warn, args)
    return itertools.ifilter(ext_filter(ignore_extensions), value)


def ext_filter(extensions):
    def ext_filter_one(filename):
        return not any(filename.endswith(ext) for ext in extensions)
    return ext_filter_one


def is_dir_or_warn(dirname):
    if os.path.isfile(dirname) or os.path.isdir(dirname):
        return True
    logging.warn('Skipping unknown file/directory "%s"', dirname)
    return False


def is_file_or_warn(filename):
    if os.path.isfile(filename):
        return True
    elif os.path.isdir(filename):
        logging.warn('Skipping directory: "%s"', filename)
    else:
        logging.warn('Skipping unknown file/directory "%s"', filename)
    return False


def list_files(file_or_dir):
    if os.path.isdir(file_or_dir):
        for directory, subdir, files in os.walk(file_or_dir):
            for filename in files:
                yield os.path.join(directory, filename)
    elif os.path.isfile(file_or_dir):
        yield file_or_dir


def get_streams(filename, options):
    if filename == '-':
        fp_in = sys.stdin
    if options.fileout == '-':
        fp_out = sys.stdout
    elif options.fileout:
        fp_out = open(options.fileout, 'w')
    if (not options.fileout) and options.append:
        fileout = filename + options.append
        while os.path.exists(fileout):
            fileout += options.append
        fp_in = open(filename)
        fp_out = open(fileout, 'w')
    elif not (options.fileout or options.append):
        if options.in_memory:
            temp_fp, fp_in = open(filename), StringIO()
            ws9.file.copy_stream(temp_fp, fp_in)
            fp_in.seek(0), temp_fp.close()
            fp_out = open(filename, 'w')
        else:
            directory, fileout = os.path.split(filename)
            new_filename = os.path.join(directory, '.' + str(get_uuid()))
            while os.path.exists(new_filename):
                new_filename = os.path.join(directory, '.' + str(get_uuid()))
            logging.info('Temporarily moving "%s" to "%s"',
                filename, new_filename)
            shutil.move(filename, new_filename)
            fp_in, fp_out = open(new_filename), open(filename, 'w')
            options.delete[filename] = new_filename
    return fp_in, fp_out


def get_file_operation(options):
    file_operation = ws9.file.FileOperation(tab_width=options.tab_width)
    file_operation.endings = options.endings
    file_operation.indent_type = options.indent_type
    return file_operation


def main():
    options, args = validate_options(PARSER.parse_args())
    file_operation = get_file_operation(options)
    for filename in get_files(options, args):
        fp_in, fp_out = get_streams(filename, options)
        if not options.in_memory:
            logging.info('Creating new file "%s"', filename)
        logging.info('Operating on "%s"', filename)
        file_operation.execute(fp_in, fp_out, close=True)
        if filename in options.delete:
            logging.info('Deleting temporary file "%s"',
                options.delete[filename])
            os.remove(options.delete[filename])
